package com.ar4j.handler;

import java.lang.reflect.Method;

import com.ar4j.ActiveRecordMetadata;
import com.ar4j.annotation.DefaultParameter;
import com.ar4j.annotation.DynamicMethod;
import com.ar4j.annotation.NamedQueryMethod;
import com.ar4j.annotation.NativeSqlMethod;
import com.ar4j.handler.DynamicMethodMetadata.DynamicMethodType;
import com.ar4j.pagination.PaginationAndSort;
import com.ar4j.util.ClassUtils;

/**
 * A factory for scanning a method and returning a DynamicMethodMetadata for it.
 */
public class DynamicMethodMetadataFactory {
  private static final String FOR_UPDATE_SUFFIX = "ForUpdate";
  
  private DynamicMethodMetadataFactory() {
    // UTILITY CLASS - NO CONSTRUCTOR FOR YOU!
  }
  
  /**
   * @return the type of dynamic method (findBy, findAllBy, countBy, etc.)
   * @throws IllegalArgumentException if the type of dynamic method is not recognized
   */
  public static DynamicMethodType getMethodType(Method method) {
    if (method.getAnnotation(NativeSqlMethod.class) != null) {
      return DynamicMethodType.NATIVE_SQL;
    } else if (method.getAnnotation(NamedQueryMethod.class) != null) {
      return DynamicMethodType.NAMED_QUERY;
    } else if(method.getName().startsWith(DynamicMethodType.FIND_BY.getMethodNamePrefix())) {
      return DynamicMethodType.FIND_BY;
    } else if(method.getName().startsWith(DynamicMethodType.FIND_ALL_BY.getMethodNamePrefix())) {
      return DynamicMethodType.FIND_ALL_BY;
    } else if(method.getName().startsWith(DynamicMethodType.COUNT_BY.getMethodNamePrefix())) {
      return DynamicMethodType.COUNT_BY;
    } else if(method.getName().startsWith(DynamicMethodType.EXISTS_BY.getMethodNamePrefix())) {
      return DynamicMethodType.EXISTS_BY;
    } else if(method.getName().startsWith(DynamicMethodType.DELETE_BY.getMethodNamePrefix())) {
      return DynamicMethodType.DELETE_BY;
    } else {
      throw new IllegalArgumentException("Method name not recognized as a dynamic method: " + method.getName());
    }
  }
  
  /**
   * Scans the given method and returns the dynamic method metadata with information about the operation 
   * to be executed
   */
  public static DynamicMethodMetadata getDynamicMethodMetadata(Method method, ActiveRecordMetadata metadata) {
    DynamicMethodMetadata out = new DynamicMethodMetadata();
    out.setMethodType(getMethodType(method));
    
    // figure out if the last argument is pagination or not
    Class<?>[] parameterTypes = method.getParameterTypes();
    int numOfRealParameters = parameterTypes.length;
    if(numOfRealParameters > 0 && PaginationAndSort.class.isAssignableFrom(parameterTypes[parameterTypes.length - 1])) {
      numOfRealParameters--;
      out.setIsLastArgumentPagination(true); 
    }
    
    // find known annotations, make sure they are mutually exclusive
    DynamicMethod dynamicAnno = method.getAnnotation(DynamicMethod.class);
    NativeSqlMethod nativeSqlAnno = method.getAnnotation(NativeSqlMethod.class);
    NamedQueryMethod namedQueryAnno = method.getAnnotation(NamedQueryMethod.class);
    if(((dynamicAnno != null ? 1 : 0) + (nativeSqlAnno != null ? 1 : 0) + (namedQueryAnno != null ? 1 : 0)) > 1) {
      throw new IllegalArgumentException("Only one dynamic method annotation is allowed for method: " + method.getName() + 
          ". DynamicMethod = " + dynamicAnno + ", NativeSqlMethod = " + nativeSqlAnno + 
          ", NamedQueryMethod = " + namedQueryAnno);
    }
    
    // either get the set up from the annotation or from scanning the name
    if(dynamicAnno != null) {
      recordParametersInMetadata(dynamicAnno.value(), dynamicAnno.defaultParameters(), numOfRealParameters, method, out, metadata, true);
    } else if (nativeSqlAnno != null) {
      recordParametersInMetadata(nativeSqlAnno.value(), nativeSqlAnno.defaultParameters(), numOfRealParameters, method, out, metadata, false);
      out.setSqlQuery(nativeSqlAnno.sqlQuery());
      out.setUpdate(nativeSqlAnno.update());
    } else if (namedQueryAnno != null) {
      recordParametersInMetadata(namedQueryAnno.value(), namedQueryAnno.defaultParameters(), numOfRealParameters, method, out, metadata, false);
      out.setQueryName(namedQueryAnno.queryName());
      out.setUpdate(namedQueryAnno.update());
    } else {
      scanMethodNameForMetadata(method, numOfRealParameters, out, metadata);
    }
    
    // deleteBy ops are always updates
    if(out.getMethodType() == DynamicMethodType.DELETE_BY) {
      out.setUpdate(true);
    }
    
    return out;
  }
  
  /**
   * Records the given parameters and default parameters into the given dynamic metadata. 
   * Optionally validates the parameter names against the given active record's metadata property names.
   */
  private static void recordParametersInMetadata(String[] parameterNames, DefaultParameter[] defaultParameters, 
      int numOfRealParameters, Method method, DynamicMethodMetadata dynamicMetadata, ActiveRecordMetadata metadata, 
      boolean validateNames) {
    
    if(parameterNames.length != numOfRealParameters) {
      throw new IllegalArgumentException("Number of parameters of method " + method.getName() + 
          " does not match the number of parameter names in the dynamic method annotation: " + parameterNames.length);
    }
    
    for(String parameter : parameterNames) {
      if(validateNames && !metadata.getPropertyNames().contains(parameter)) {
        throw new IllegalArgumentException("Unknown field in parameters: " + parameter + " for base class: " + 
            metadata.getBaseClass() + " in dynamic method: " + method.getName());
      }
      
      dynamicMetadata.getParameters().add(parameter);
    }
    
    for(DefaultParameter defaultParameter : defaultParameters) {
      if(validateNames && !metadata.getPropertyNames().contains(defaultParameter.name())) {
        throw new IllegalArgumentException("Unknown field in default parameters: " + defaultParameter.name() + " for base class: " + 
            metadata.getBaseClass() + " in dynamic method: " + method.getName());
      }
      
      Object value = ClassUtils.coerceValueToType(defaultParameter.value(), defaultParameter.valueType());
      dynamicMetadata.getDefaultParameters().put(defaultParameter.name(), value);
    }
  }
  
  /**
   * Scans the method name (findAllByXAndYAndZ) to find information about the dynamic operation process.
   */
  private static void scanMethodNameForMetadata(Method method, int numOfRealParameters, 
      DynamicMethodMetadata dynamicMetadata, ActiveRecordMetadata metadata) {
    
    // cut off the suffix if needed
    String dynamicName = method.getName();
    if(dynamicMetadata.getMethodType() == DynamicMethodType.FIND_BY && dynamicName.endsWith(FOR_UPDATE_SUFFIX)) {
      dynamicMetadata.setIsForUpdate(true);
      dynamicName = dynamicName.substring(0, dynamicName.length() - FOR_UPDATE_SUFFIX.length());
    }
    
    // cut off the prefix
    dynamicName = dynamicName.substring(dynamicMetadata.getMethodType().getMethodNamePrefix().length());
    
    // split into parameters
    for(String field : dynamicName.split("And")) {
      String lowercasedFirst = field.substring(0, 1).toLowerCase() + field.substring(1);
      if(!metadata.getPropertyNames().contains(lowercasedFirst)) {
        throw new IllegalArgumentException("Unknown field in parameters: " + lowercasedFirst + " for base class: " + 
            metadata.getBaseClass() + " in dynamic method: " + method.getName());
      }
      
      dynamicMetadata.getParameters().add(lowercasedFirst);
    }
    
    // validate number of parameters
    if(dynamicMetadata.getParameters().size() != numOfRealParameters) {
      throw new IllegalArgumentException("Number of parameters of method " + method.getName() + 
          " does not match the number of parameter names in the method name: " + dynamicMetadata.getParameters().size());
    }
    
  }
}
